學過資料結構的朋友應該都知道 棧 stack
的概念,棧就像一個開口向上的容器,可以將數據放入和取出,由於開口只有一邊,所以有 後進先出 LIFO
的特性,先放入的數據會被壓在下方,需要等上面的都被拿完後才能取出。
棧示意圖:
段暫存器有四個 CS、DS、SS、ES,前面已經介紹過 CS 代碼段暫存器和 DS 數據段暫存器,除去 ES 外,剩最後一個 SS 棧段暫存器。
8086 CPU 透過 SS 棧段暫存器
和 SP 棧指標暫存器
實現棧的機制,SS + SP
會指向棧的 頂部
,加入元素時將 SS - 2
、取出元素時將 SS + 2
,棧的操作是以 字
為單位,不能像 mov 一樣操作字節,所以加入和取出是以 2 個字節為單位。
有沒有人發現哪裡怪怪的,為什麼加入元素是用減而不是用加,因為組合語言棧的方向和我們習慣的陣列相反,陣列索引由 0 開始向後遞增,而棧是由 記憶體高位開始向低位遞減
,所以加入元素才會用減的。
假設將記憶體 10000 到 1000F 當作棧,示意圖如下:
棧頂初始時會位於棧的 最後一格再加一的位置
,加入元素就往上移動 2 格、取出元素就往下移動 2 格。這裡的取出元素其實只是移動 SS 指標的位置,並不會真正的清除記憶體上的資料,只有當下次加入資料時才會將原資料覆蓋掉。
8086 CPU 提供了兩個指令來操作棧。
push 指令可分解為,先將 SS - 2,再將元素加入棧。
pop 指令可分解為,先將元素從棧中取出,再將 SS + 2。
用法:
push ax ; push 暫存器
pop ax ; pop 暫存器
push ds ; push 段暫存器
pop ds ; pop 段暫存器
mov ax, 2000H
mov ds, ax ; 使用記憶體偏移地址需設置 ds 數據段暫存器
push [0] ; push 記憶體
pop [0] ; pop 記憶體
接著用 Debug 工具執行下列指令,並將 SP 設為 0000H
看看。
mov ax, ff01
push ax
pop bx
觀察下方第一個 t 命令,因為執行 mov ax, FF01,AX 暫存器的值被改為 FF01。
接著執行 push 指令,因為 SP 的值為 0000 已經在整個棧的最頂端,所以再減 2 就發生了溢位,SP 跑到了底部 FFFE 的位置,由此可以看出 棧是一個循環結構
,如果超過範圍就會從另一端開始,要小心不要因為循環而覆蓋到重要數據造成程式出錯。
最後執行 pop 指令後,SP 恢復到 0000,BX 正確被改變為 FF01。
這裡的棧越界不是上面提到的 SP 溢位,而是透過 SS + SP
我們只能知道棧頂的位置,並不能知道這個棧的範圍和大小,例如 C++ 的 stack 就會去紀錄棧的大小,如果想從空的棧中取出數據就會引發例外狀況,如下。
而 8086 CPU 的棧並沒有這樣的機制,我們並不能去設定棧的大小,只能知道棧頂的位置在哪,寫程式時我們會定義數據段、代碼段和棧段,如果不小心棧越界了,就可能覆蓋到其他段的數據造成程式出錯,因此在規劃棧段時需特別注意空間的配置以防棧越界。
棧在組合語言中是個很重要的概念用途很廣,這樣講大家可能無法體會,舉 C 語言的函數為例,呼叫函數時會將當前暫存器的值和區域變數入棧,返回時再透過出棧還原之前的狀態,遞迴的呼叫也是這個原理,平常寫程式可能很少用到,但程式底層的應用卻非常廣,今天就到這裡摟,感謝大家觀看。
stack 不是叫堆疊嗎?
棧這翻譯好像第一次聽過@@